#-*- coding: utf-8 -*-

#This file is part of pyLot library.
#
# pyLot is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyLot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pyLot.  If not, see <http://www.gnu.org/licenses/>.

__author__ = u'Pierre Puiseux, Guillaume Baty'
__copyright__ = u"Copyright 2011-2012 (C) andheo, Université de Pau et des Pays de l'Adour"
__credits__ = [u'Pierre Puiseux', u'Guillaume Baty']
__license__ = "GNU Lesser General Public License"

__all__ = [
  'DatumDelegate',
  'DatumItem',
  'DatumModel',
  'GuiDatumTableView',
  'GuiEditToolBox',
  'GuiFilterToolBox',
  'GuiContainerTreeView',
  'ContainerModel',
  ]

import copy
import datetime

import gc

from pyLot.core import make_alias, Container, Datum, clean_memory, check_type

from Qt4 import QtGui, QtCore
from Qt4.compat import from_qvariant

from _pylot.qtgui.widgets.gui_typedchoosers import make_selector, get_paint_function
from _pylot.qtgui.widgets.gui_typedfilters import make_filter
from _pylot.qtgui.custom import DEFAULT_CUSTOM
from _pylot.qtgui.widgets.gui_containereditor import GuiDialogContainerEditor
from _pylot.qtgui.widgets.gui_dialog import GuiDialog
from _pylot.qtgui.default.impl_widget import Widget
from _pylot.qtgui.spyder.gui_listmanager import GuiListSelector
from _pylot.qtgui.styles import policy
from _pylot.qtgui.tools import get_icon


class DoNotMatchError(Exception):
  pass

class ContainerList(list):
  def __init__(self, containers=[]):
    for container in containers :
      check_type(u'container', container, Container)
    list.__init__(self, containers)

  def clear(self):
    for container in self :
      self.remove(container)

class GuiContainerTreeView(QtGui.QTreeView):

  def __init__ (self, model, custom=DEFAULT_CUSTOM):
    QtGui.QTreeView.__init__(self)

    self.custom = custom
    self._clic_point = None

    self.setModel(model)
#    self.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)

    delegate = ContainerDelegate(custom=custom)
    self.setItemDelegate(delegate)

    self.collapsed.connect(self.viewCollapsed)
    self.expanded.connect(self.viewExpanded)

    self.expandAll()

  def viewCollapsed(self, item):
    self.fineTune()

  def viewExpanded(self, item):
    self.fineTune()

  def fineTune(self):
    for col in range(self.model().columnCount()):
      self.resizeColumnToContents(col)


class GuiDatumTableView(QtGui.QTableView):

  editItemsFinished = QtCore.Signal()

  def __init__ (self, model, custom=DEFAULT_CUSTOM):
    QtGui.QTableView.__init__(self)

    self.custom = custom
    self._clic_point = None

    self.setModel(model)
#    self.setEditTriggers(QtGui.QAbstractItemView.CurrentChanged)
    self.setEditTriggers(QtGui.QAbstractItemView.AnyKeyPressed | QtGui.QAbstractItemView.DoubleClicked | QtGui.QAbstractItemView.EditKeyPressed)

    self.viewport().installEventFilter(self)
    delegate = DatumDelegate(custom=custom)
    self.setItemDelegate(delegate)

#    self.contextMenuEvent = self.pdmContextMenuEvent
    self.vheader = self.verticalHeader()
    self.vheader.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
    self.connect(self.vheader, QtCore.SIGNAL('customContextMenuRequested(const QPoint&)'),
                 self.rowContextMenuEvent)

    self.fineTune()

  def setAllRowsVisible(self, visible=True):
    for row in range(self.model().rowCount()):
      self.setRowHidden(row, not visible)

  def applyFilters(self, filters, rows=None):
    vfilters = {}

    # reduce filters to enabled filters
    ALL_FILTERS_DISABLED = True
    for col, _filters in filters.items():
      col_filters = []
      for filt in _filters :
        if filt.isChecked():
          col_filters.append(filt)
          ALL_FILTERS_DISABLED = False
      if col_filters :
        vfilters[col] = col_filters


    if rows is None :
      rows = [i for i in range(self.model().rowCount())]

    if ALL_FILTERS_DISABLED :
      # display all rows
      for row in rows :
        self.setRowHidden(row, False)
    else :
      # search matching rows
      for row in rows :
        try :
          for column in vfilters:
            item = self.model().item(row, column)
#            item = self.model().itemFromIndex(idx)
            for filt in filters[column] :
              if not filt.match(item.datum.value):
                raise DoNotMatchError
        except DoNotMatchError :
          self.setRowHidden(row, True)
        else :
          self.setRowHidden(row, False)

  def fineTune(self):
    model = self.model()
    container = self.model().newContainer()
    titles = [make_alias(datum) for datum in container.iterDatumValues()]
    model.setHorizontalHeaderLabels(titles)
    self.resizeColumnsToContents()
    clean_memory()

  def contextMenuEvent(self, event):
    menu = QtGui.QMenu()
    action_edit_items = QtGui.QAction(u'Éditer les éléments selectionnés', self)
    action_edit_items.triggered.connect(self.actionEditItems)
    menu.addAction(action_edit_items)

    ok = True
    for idx in self.selectedIndexes():
      item = self.model().itemFromIndex(idx)
      if item.datum.nature is not unicode :
        ok = False
        break

    if ok :
      action_select_item_value = QtGui.QAction(u'Selectionner une valeur dans la liste', self)
      action_select_item_value.triggered.connect(self.actionSelectItemValue)
      menu.addAction(action_select_item_value)

    menu.exec_(event.globalPos())

  def rowContextMenuEvent(self, point):
    self._clic_point = point

    model = self.model()
    data = model.containers[self.vheader.logicalIndexAt(point)]

    menu = QtGui.QMenu()
    action_edit = QtGui.QAction(u'Edit %s' % make_alias(data), self)
    action_edit.triggered.connect(self.actionEditRow)
    menu.addAction(action_edit)
    menu.exec_(self.vheader.mapToGlobal(point), action_edit)

  def actionEditRow(self):
    model = self.model()
#    pt = self.vheader.mapFromGlobal(self.cursor().pos())
    if self._clic_point is None :
      return
    row = self.vheader.logicalIndexAt(self._clic_point)
    self.selectRow(row)
    dialog = GuiDialogContainerEditor(model.containers[row],
      mode=GuiDialogContainerEditor.ModeManualSync, custom=self.custom)
    dialog.exec_()
    self._clic_point = None

  def actionEditItems(self):
    model = self.model()
    same_data_objects = True

    selection = self.selectedIndexes()
    nitems = len(selection)
    if nitems < 1 :
      return
    elif nitems == 1 :
      pass
    else :
      # Check user tries to edit same objects
      for idx in range(1, nitems):
        d0 = model.itemFromIndex(selection[idx - 1]).datum
        d1 = model.itemFromIndex(selection[idx]).datum
        if d0.name != d1.name :
          same_data_objects = False
          break

      if same_data_objects is False :

        icon = QtGui.QMessageBox.Critical
        title = u'Modification impossible'
        text = u"""
        Vous ne pouvez pas éditer des données de type différent.
        Les cellules non compatibles sont les suivantes
          - %s (valeur=%s)
          - %s (valeur=%s)
        """ % (
          make_alias(d0), d0.value,
          make_alias(d1), d1.value,
          )
        mbox = QtGui.QMessageBox(icon, title, text)
        mbox.exec_()
        return

    i0 = selection[0]
    item = model.itemFromIndex(i0)
    tmpdatum = copy.deepcopy(item.datum)
    selector = make_selector(tmpdatum, custom=self.custom)
    dialog = GuiDialog(selector)
    rep = dialog.exec_()

    if rep == QtGui.QDialog.Accepted and selector.isEnabled() :
      for index in selection:
        item = model.itemFromIndex(index)
        item.datum.value = tmpdatum.value
    else :
      return

    self.editItemsFinished.emit()

  def actionSelectItemValue(self):
    model = self.model()
    selection = self.selectedIndexes()
    nitems = len(selection)
    if not nitems :
      return

    value_list = set([])
    for idx in selection :
      item = model.itemFromIndex(idx)
      value_list.update(item.datum.value_list)

    lst = list(value_list)
    lst.sort()
    widget = GuiListSelector(lst)
    widget.listwidget.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)

    dialog = GuiDialog(widget)
    dialog.resize(QtCore.QSize(300, 450))
    res = dialog.exec_()
    if res :
      value = widget.getValue()
      for idx in selection :
        item = model.itemFromIndex(idx)
        item.datum.value = value

  def selectedRows(self):
    rows = set([])
    for idx in self.selectedIndexes():
      rows.add(idx.row())
    rows = list(rows)
    rows.sort()
    return rows

  def createFilterToolBox(self, custom=None):
    if custom is None:
      custom = self.custom
    return GuiFilterToolBox(self.model(), [self], custom=custom)

  def createEditToolBox(self, buttons=['new', 'remove', 'clear']):
    return GuiEditToolBox(self.model(), [self], buttons=buttons)


class ContainerModel(QtGui.QStandardItemModel):
  def __init__(self):
    QtGui.QStandardItemModel.__init__(self)
    self.setColumnCount(2)

  def addContainer(self, container):
    root = ContainerItem(container)
    self.insertRow(0, root)


class DatumModel(QtGui.QStandardItemModel):
  def __init__(self, factory):
    QtGui.QStandardItemModel.__init__(self)
    self.containers = ContainerList()
    self.factory = factory
    self.count = 0

  def clear(self, *args, **kwargs):
    self.removeContainerRows([row for row in range(0, self.rowCount())])

  def sort(self, column, order):
    raise NotImplementedError
    QtGui.QStandardItemModel.sort(self, column, order)

  def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
    if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Vertical :
      try :
        alias = make_alias(self.containers[section])
      except IndexError :
        return QtGui.QStandardItemModel.headerData(self, section, orientation, role)
      else :
        return alias
    else :
      return QtGui.QStandardItemModel.headerData(self, section, orientation, role)

  def appendContainerRow(self, container):
    if container.name is None :
      raise ValueError, u'Only named container are allowed'
    items = []
    for i, datum in enumerate(container.datumValues()) :
      items.append(DatumItem(datum))
    self.containers.append(container)
    self.count += 1
    self.appendRow(items)

  def newContainer(self, **kargs):
    default = dict(
      row=self.rowCount(),
      date=datetime.datetime.today(),
      count=self.count
      )
    default.update(kargs)
    return self.factory(**default)

  def newContainerRow(self, **kargs):
    self.appendContainerRow(self.newContainer(**kargs))

  def removeContainerRow(self, row):
    self.removeContainerRows([row])

  def removeContainerRows(self, rows, count=1):

    containers = {}
    first_items = {}

    # clean models
    for row in rows :
      # clean items & connections
      for col in range(self.columnCount()):
        item = self.item(row, col)
        item.deleteConnexions()

      # mark items
      first_items[row] = self.item(row, 0)
      # mark container
      containers[row] = self.containers[row]


    for row in rows:
      # remove ContainerModel row
      container = containers[row]
      self.containers.remove(container)
      # remove QStandardItemModel row
      self.removeRow(first_items[row].row())

    del containers

    clean_memory()

  def rowToDict(self, row):
    kargs = {}
    for col in range(self.columnCount()):
      idx = self.index(row, col)
      item = self.itemFromIndex(idx)
      kargs[item.datum.name] = item.datum.value
    return kargs

  def rowsToDict(self):
    for row in range(self.rowCount()):
      yield self.rowToDict(row)

  def colToList(self, col):
    lst = []
    for row in range(self.rowCount()):
      idx = self.index(row, col)
      item = self.itemFromIndex(idx)
      lst.append(item.datum.value)
    return lst

  def colsToList(self):
    for col in range(self.columnCount()):
      yield self.colToList(col)

  def toContainer(self):
    container = Container(u'model')
    for row in range(self.rowCount()):
      crow = copy.deepcopy(self.containers[row])
      crow.name = u'row_%06d_%s' % (row, crow.name)
      container.add(crow)
    return container


class DatumItem(QtGui.QStandardItem):
  def __init__(self, datum):
    QtGui.QStandardItem.__init__(self, unicode(datum.value))
    self.datum = datum
    self.createConnexions()

  def createConnexions(self):
    self.datum.connect(u'valueChanged', self.datumValueChanged)

  def deleteConnexions(self):
    self.datum.disconnect(u'valueChanged', self.datumValueChanged)

  def datumValueChanged(self, datum, old, new):
    self.setData(datum.value, QtCore.Qt.DisplayRole)
    self.setData(datum.value, QtCore.Qt.EditRole)

class DatumNameItem(QtGui.QStandardItem):
  def __init__(self, datum):
    QtGui.QStandardItem.__init__(self, make_alias(datum))
    self.datum = datum

class ContainerItem(QtGui.QStandardItem):
  def __init__(self, container):
    QtGui.QStandardItem.__init__(self, make_alias(container))
    self.container = container
    for i, obj in enumerate(container.values()):
      if isinstance(obj, Container):
        item = ContainerItem(obj)
        self.setChild(i, item)
      elif isinstance(obj, Datum):
        name = DatumNameItem(obj)
        value = DatumItem(obj)
        self.setChild(i, 0, name)
        self.setChild(i, 1, value)
      else :
        continue

    self.createConnexions()

  def createConnexions(self):
    pass

  def deleteConnexions(self):
    pass

  def containerNameChanged(self, container, old, new):
    self.setData(container.name, QtCore.Qt.DisplayRole)
    self.setData(container.name, QtCore.Qt.EditRole)


class DatumDelegate(QtGui.QStyledItemDelegate):
  def __init__(self, custom=DEFAULT_CUSTOM):
    self.custom = custom
    super(DatumDelegate, self).__init__(None)

  def createEditor(self, parent, option, index):
    model = index.model()
    item = model.itemFromIndex(index)
    editor = make_selector(item.datum, custom=self.custom)
    editor.installEventFilter(self)
    editor.setParent(parent)
    return editor

  def setEditorData(self, editor, index):
    model = index.model()
    item = model.itemFromIndex(index)
    editor.setValue(item.datum.value)

  def paint(self, painter, option, index):
    model = index.model()
    item = model.itemFromIndex(index)

    paint_cell = get_paint_function(item.datum, self.custom)
    if paint_cell :
      ok = paint_cell(self, painter, option, index, item.datum)
      if not ok :
        QtGui.QStyledItemDelegate.paint(self, painter, option, index)
    else :
      QtGui.QStyledItemDelegate.paint(self, painter, option, index)

  def setModelData(self, editor, model, index):
    # Update field
    value = editor.value()
    item = model.itemFromIndex(index)
    item.datum.value = value

  def updateEditorGeometry(self, editor, option, index):
    editor.setGeometry(option.rect)

class ContainerDelegate(DatumDelegate):
  def createEditor(self, parent, option, index):
    model = index.model()
    item = model.itemFromIndex(index)
    if isinstance(item, DatumItem):
      return DatumDelegate.createEditor(self, parent, option, index)
    else:
      return QtGui.QStyledItemDelegate.createEditor(self, parent, option, index)

  def setEditorData(self, editor, index):
    model = index.model()
    item = model.itemFromIndex(index)
    if isinstance(item, DatumItem):
      return DatumDelegate.setEditorData(self, editor, index)
    else:
      return QtGui.QStyledItemDelegate.setEditorData(self, editor, index)


  def paint(self, painter, option, index):
    model = index.model()
    item = model.itemFromIndex(index)
    if isinstance(item, DatumItem):
      return DatumDelegate.paint(self, painter, option, index)
    else:
      return QtGui.QStyledItemDelegate.paint(self, painter, option, index)

  def setModelData(self, editor, model, index):
    model = index.model()
    item = model.itemFromIndex(index)
    if isinstance(item, DatumItem):
      return DatumDelegate.setModelData(self, editor, model, index)
    else:
      return QtGui.QStyledItemDelegate.setModelData(self, editor, model, index)

  def updateEditorGeometry(self, editor, option, index):
    editor.setGeometry(option.rect)


class GuiFilterToolBox(QtGui.QWidget, Widget):
  def __init__(self, model, views, custom=DEFAULT_CUSTOM):
    QtGui.QWidget.__init__(self)
    Widget.__init__(self)
    self.model = model
    self.views = views
    self.custom = custom

    self.layout_main = QtGui.QVBoxLayout(self)

    self.widget_filters = QtGui.QWidget()
    self.layout_filters = QtGui.QHBoxLayout(self.widget_filters)

    ncol = len(self.model.newContainer())
    self.filters = {}
    for col in range(ncol):
      self.filters[col] = []

    self.l_title = QtGui.QCheckBox()

    self.connect(self.model, QtCore.SIGNAL('itemChanged(QStandardItem *)'),
                 self.itemChanged)
    self.l_title.toggled.connect(self.toggleFilters)

    self.makeFilters()

    self.layout_main.addWidget(self.l_title)
    self.layout_main.addWidget(self.widget_filters)
    self.layout_filters.addStretch()

    self.layout_main.setContentsMargins(0, 0, 0, 0)
    self.layout_main.setSpacing(0)
    self.setContentsMargins(0, 0, 0, 0)
    self.widget_filters.setContentsMargins(0, 0, 0, 0)
    self.layout_filters.setContentsMargins(0, 0, 0, 0)


    self._refresh()
    self._translate()
    self._fineTune()

  def _refresh(self):
    for filters in self.filters.values() :
      for filt in filters :
        filt.refresh()

  def toggleFilters(self, state):
    self.widget_filters.setVisible(state)
    if state is False :
      for filters in self.filters.values() :
        for filt in filters :
          filt.setChecked(False)

  def itemChanged(self, item):
    for view in self.views :
      view.applyFilters(self.filters, [item.row()])

  def actionFilter(self):
    for view in self.views :
      view.applyFilters(self.filters)

  def makeFilters(self):
    ncol = len(self.model.newContainer())
    for column in range(ncol):
      self.makeFilter(column)

  def makeFilter(self, column):
    datum = self.model.newContainer()[column]

    widget = make_filter(datum, custom=self.custom)
    widget.filterChanged.connect(self.actionFilter)
    self.filters.setdefault(column, []).append(widget)
    self.layout_filters.addWidget(widget)

  def _fineTune(self):
    self.toggleFilters(False)

  def _translate(self):
    self._setTexts([
      (self.l_title, self.tr_(u"Filtres d'affichage")),
      ])


class GuiEditToolBox(QtGui.QWidget, Widget):

  def __init__(self, model, views, buttons=['new', 'remove', 'clear']):
    QtGui.QWidget.__init__(self)
    Widget.__init__(self)
    self.model = model
    self.views = views

    self.layout_main = QtGui.QHBoxLayout(self)

    self.filters = {}

    icons = dict(new=u'add.png', remove=u'remove.png',
                 clear=u'cnrdelete-all.png')

    for i, pb_name in enumerate(buttons):#, 'infos', 'toggle') :
      if pb_name not in icons :
        continue
      pb = QtGui.QPushButton(pb_name)
      pb.setFlat(True)
      pb.setSizePolicy(policy.HSMALL)
      pb.setIcon(get_icon(icons[pb_name]))
      pb.clicked.connect(getattr(self, 'action%s' % pb_name.capitalize()))
      self.layout_main.addWidget(pb)
    self.layout_main.addStretch()

    self.layout_main.setContentsMargins(0, 0, 0, 0)
    self.layout_main.setSpacing(0)
    self.setContentsMargins(0, 0, 0, 0)

  def actionNew(self):
    self.model.newContainerRow()
    for view in self.views :
      view.resizeColumnsToContents()

  def actionRemove(self):
    rows = set([])
    for view in self.views :
      for index in view.selectedIndexes():
        rows.add(index.row())
    self.model.removeContainerRows(rows)

  def actionClear(self):
    nrow = self.model.rowCount()
    if nrow :
      msg = QtGui.QMessageBox()
      msg.setText(u"Remove all items");
      msg.setInformativeText(u'Do you really want to remove ALL items ?');
      msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)
      msg.setDefaultButton(QtGui.QMessageBox.No)
      res = msg.exec_()
      if res == QtGui.QMessageBox.Yes :
        rows = range(nrow)
        self.model.removeContainerRows(rows)
